package com.cordys.opensource.crom;

import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.cordys.cpc.bsf.busobject.BusObject;
import com.cordys.cpc.bsf.classinfo.AttributeInfo;
import com.cordys.cpc.bsf.classinfo.ClassInfo;
import com.cordys.cpc.bsf.classinfo.RelationInfo_Composite;
import com.cordys.cpc.bsf.classinfo.RelationInfo_FK;
import com.cordys.cpc.bsf.event.AfterAbortObjectEvent;
import com.cordys.cpc.bsf.event.AfterCommitObjectEvent;
import com.cordys.cpc.bsf.event.BeforeAbortObjectEvent;
import com.cordys.cpc.bsf.event.BeforeCommitObjectEvent;
import com.cordys.cpc.bsf.event.BusObjectEvent;
import com.cordys.cpc.bsf.event.BusObjectUpdateEvent;
import com.cordys.cpc.bsf.event.IAfterAbortObjectListener;
import com.cordys.cpc.bsf.event.IAfterCommitObjectListener;
import com.cordys.cpc.bsf.event.IAfterDeleteListener;
import com.cordys.cpc.bsf.event.IAfterInsertListener;
import com.cordys.cpc.bsf.event.IAfterUpdateListener;
import com.cordys.cpc.bsf.event.IBeforeAbortObjectListener;
import com.cordys.cpc.bsf.event.IBeforeCommitObjectListener;
import com.cordys.cpc.bsf.event.IBeforeDeleteListener;
import com.cordys.cpc.bsf.event.IBeforeInsertListener;
import com.cordys.cpc.bsf.event.IBeforeUpdateListener;

/**
 * <p>This is a framework class that contains metadata about a certain
 * type of domain object.
 * It typically contains information about the relationship between various
 * tables in the database and their corresponding domain object.
 * </p>
 * E.g., for a Ticket, it will refer to 
 * <ul>
 * <li>crom.example.db.Ticket table</li>
 * <li>crom.example.db.TicketRevision table</li>
 * </ul>
 * 
 * In runtime we have 2 phases:
 * <ul>
 * <li> Startup phase. This is launched when the WS-AppServer Service Container is started. During this phase we read the WS-AppServer metadata information
 *  (ClassInfo) and analyse and prepare the relationship information into the Metadata object.
 * <li> Operational phase. During this phase, updates and inserts on the domain objects come in, and the these events
 * will be "forwarded" to the DomainObjectInformation class. This class acts as a sort of a wrapper around domain objects, and takes
 * care of appropriate persistence mappings. 
 * </ul>   
 * @see DomainObject
 * @see RevisionedObject
 * @see BusObjectWrapper
 * @see ObjectRegistry
 */
public class BusObjectMetadata implements IAfterAbortObjectListener, IAfterCommitObjectListener, IBeforeInsertListener, IAfterInsertListener, IBeforeUpdateListener, IAfterUpdateListener, IAfterDeleteListener, IBeforeDeleteListener, IBeforeAbortObjectListener, IBeforeCommitObjectListener
{
	/**
	 * Returns the listener instance for the specified domain class, or null if not present. In practice all domain objects
	 * have a listener registered, unless a class is added to the classpath after the WS-AppServer has been started.
	 * The listener can be used to retrieve further metadata.
	 * @param domainClass The class for which to retrieve the metadata.
	 * @return Returns the listener instance for the specified domain class, or null if not present.
	 */
	public static BusObjectMetadata getMetadata(Class<? extends BusObject> domainClass)
	{
		return CROMConnector.getMetadata(domainClass);
	}
	
	/**
	 * Determines whether the class (or one of it's super classes) has the DomainObject annotation
	 * @param clazz
	 * @return
	 */
	public static boolean isDomainObject(Class<?> clazz)
	{
		if (clazz==null) return false;
		if (clazz.isAnnotationPresent(DomainObject.class)) return true;
		return isDomainObject(clazz.getSuperclass());
	}
	
	/**
	 * Determines whether the class (or one of it's super classes) has the RevisionedObject annotation
	 * @param clazz
	 * @return
	 */
	public static boolean isRevisionedObject(Class<?> clazz)
	{
		if (clazz==null) return false;
		if (clazz.isAnnotationPresent(RevisionedObject.class)) return true;
		return isRevisionedObject(clazz.getSuperclass());
	}
	
	/**
	 * Determines whether the class (or one of it's super classes) has the MasterData annotation
	 * @param clazz
	 * @return
	 */
	public static boolean isMasterDataObject(Class<?> clazz)
	{
		if (clazz==null) return false;
		if (clazz.isAnnotationPresent(MasterData.class)) return true;
		return isMasterDataObject(clazz.getSuperclass());
	}
	
	/**
	 * The domain class, e.g. crom.example.domain.Ticket, crom.example.domain.User, etc.
	 */
	private final Class<? extends BusObject> domainClass;
	/**
	 * The class that belongs to the database representation of the domain object. Typically used for quick instantiation.
	 */
	private final Class<? extends BusObject> dbClass;
	/**
	 * ClassInfo of the domain object. E.g., for Ticket this contains the metadata of the crom.example.domain.Ticket class 
	 */
	private final ClassInfo domainClassInfo;
	/**
	 * The WS-AppServer metadata of the database class for the domain object.
	 */
	private final ClassInfo dbClassInfo;
	/**
	 * Metadata for the revision table containing Fact information (i.e., the information of the incoming message).
	 * This information is continuously improved by the users of the system. Each improvement is stored as a separate
	 * record in the table (according to the so-called revisioning pattern).
	 * E.g., for Ticket, this is "crom.example.db.TicketRevision.class".
	 */
	private final Class<? extends BusObject> dbRevisionClass;
	/**
	 * ClassInfo of the revision table with the fact information for the domain object. E.g., for Ticket this contains the metadata of
	 * the TicketRevision table
	 */
	private final ClassInfo dbRevisionClassInfo;
	/**
	 * Contains a list of all possible types of children for this type of domain object.
	 */
	private final Map<String, BusObjectMetadata> domainChildren = new LinkedHashMap<String, BusObjectMetadata>();
	/**
	 * Parent metadata
	 */
	private final BusObjectMetadata parentMetadata;
	/**
	 * Relation from parent to us
	 */
	private final RelationInfo_Composite relationFromParent;
	/**
	 * [Optional] mapping class between this metadata and it's parent
	 */
	private Class<? extends BusObject> mappingClass;
	/**
	 * [Optional] foreign key relationship between this metadata and it's parent
	 */
	private RelationInfo_FK fkParent;
	
	
	/**
	 * The current crom in which we run
	 */
	private final CROMConnector crom;
	
	BusObjectMetadata(Class<? extends BusObject> domainClass, CROMConnector crom)
	{
		this(domainClass, crom, null, null);
	}
	
	private BusObjectMetadata(Class<? extends BusObject> domainClass, CROMConnector crom, RelationInfo_Composite relationFromParent, BusObjectMetadata parentMetadata)
	{
		this.crom = crom;
		this.parentMetadata = parentMetadata;
		this.relationFromParent = relationFromParent;
		
		// Check and set the domain class information
		this.domainClass = domainClass;
		this.domainClassInfo = BusObject._getClassInfo(domainClass);
		checkDomainClassInfo();
		
		// Find and check the mapping to the database class.
		this.dbClass = findDbClass();
		this.dbClassInfo = BusObject._getClassInfo(dbClass);
		checkUIDElements(dbClassInfo);

		// If the domain class implements the Revisioning pattern we have to keep some extra administration.
		if (isRevisionedObject(domainClass))
		{
    		this.dbRevisionClass = findDbRevisionClass();
    		this.dbRevisionClassInfo = BusObject._getClassInfo(dbRevisionClass);
    		checkRevisioningPattern();
		}
		else
		{
			this.dbRevisionClass = null;
			this.dbRevisionClassInfo = null;
		}
		
		// Determine relationship with parent
		checkParentInfo();
		
		
		// Add all children
		Enumeration<?> allRelations = domainClassInfo.getRelationInfos();
		while (allRelations.hasMoreElements())
		{
			Object o = allRelations.nextElement();
			if (! (o instanceof RelationInfo_Composite)) continue; // Not a relation we can work with.
			RelationInfo_Composite relation = (RelationInfo_Composite) o;
			try
			{
				@SuppressWarnings("unchecked") // ... but it would be real strange if it isn't a bus object
				Class<? extends BusObject> childClass = relation.getRelatedClass();
				if (isDomainObject(childClass) && !isParent(childClass)) // ... if it is a @DomainObject we can do something with it, but only if it it not also a parent (avoiding recursion)
				{
					domainChildren.put(relation.getName(), new BusObjectMetadata(childClass, crom, relation, this));
				}
			}
			catch (ClassNotFoundException cnfe)
			{
				// can't do anything with this relation, let's just ignore.
				//  Apparently something went wrong during startup and classloading
			}
		}
	}
	
	/**
	 * Check whether the clazz is perhaps also available in the parent hierarchy
	 * of this domain class. Useful to avoid endless recursion.
	 * @param clazz
	 * @return
	 */
	private boolean isParent(Class<? extends BusObject> clazz)
	{
		if (this.domainClass.equals(clazz))
		{
			return true;
		}
		if (this.parentMetadata!=null)
		{
			return parentMetadata.isParent(clazz);
		}
		return false;
	}
	
	@Deprecated // For compatibility
	BusObjectMetadata getDomainChild(Class<? extends BusObject> childClass)
	{
		// Search for first match.
		Collection<BusObjectMetadata> children = getDomainChildren();
		for (BusObjectMetadata child : children)
		{
			if (child.domainClass.equals(childClass))
			{
				return child;
			}
		}
		return null;
	}
	
	/**
	 * Returns the metadata of the parent (can be null).
	 * @return
	 */
	BusObjectMetadata getParentMetadata()
	{
		return parentMetadata;
	}
	
	/**
	 * Returns the relationship of this metadata with it's parent.
	 * @return
	 */
	RelationInfo_Composite getParentRelationship()
	{
		return relationFromParent;
	}
	
	/**
	 * Returns the database relationship of this metadata with it's parent.
	 * @return
	 */
	RelationInfo_FK getParentForeignKey()
	{
		return fkParent;
	}
	
	/**
	 * Returns the metadata for the child with the specified relation name.
	 * @param relation
	 * @return
	 */
	BusObjectMetadata getRelation(String relation)
	{
		return domainChildren.get(relation);
	}
	
	/**
	 * Returns the metadata for the child with the specified class. Finds the first one available.
	 * @param relation
	 * @return
	 */
	@Deprecated
	BusObjectMetadata getRelation(Class<? extends BusObject> relation)
	{
		for (BusObjectMetadata child : domainChildren.values())
		{
			if (child.domainClass.equals(relation))
			{
				return child;
			}
		}
		return null;
	}
	
	/**
	 * Returns the list of children that can be aggregated into this @DomainObject.
	 * @return
	 */
	Collection<BusObjectMetadata> getDomainChildren()
	{
		return domainChildren.values();
	}
	
	/**
	 * Returns a foreign key relation based on the specified name, according to the crom naming conventions.
	 * The convention is that at least one of the attributes has to contain the relationName
	 * @param fkRelations
	 * @param relationName
	 * @return
	 */
	private RelationInfo_FK getNamedRelation(List<RelationInfo_FK> fkRelations, String relationName)
	{
		if (fkRelations==null)
		{
			return null;
		}
		for (RelationInfo_FK fkRelation: fkRelations)
		{
			String[] remoteAttributes = fkRelation.getRelatedAttributes();
			for (String attributeName : remoteAttributes)
			{
				if (attributeName.contains(relationName))
				{
					return fkRelation;
				}
			}
		}
		return null;
	}	
	
	/**
	 * Returns a the first foreign key relation encountered.
	 * @param fkRelations
	 * @param relationFromParentName
	 * @return
	 */
	private RelationInfo_FK getAnyRelation(List<RelationInfo_FK> fkRelations, String relationFromParentName)
	{
		if (fkRelations==null)
		{
			return null;
		}
		if (fkRelations.isEmpty()) // None is found.
		{
			return null;
		}
		return fkRelations.get(0);
	}
	
	private Class<? extends BusObject> getMappingRelation(Class<? extends BusObject> parentClass)
	{
		HashMap<Class<? extends BusObject>, List<RelationInfo_FK>> parentKeys = BusObjectHelper.getForeignKeyRelations(parentClass);
		HashMap<Class<? extends BusObject>, List<RelationInfo_FK>> objectKeys = BusObjectHelper.getForeignKeyRelations(dbClass);
		for (Class<? extends BusObject> potentialMappingClass : parentKeys.keySet())
		{
			List<RelationInfo_FK> objectKey = objectKeys.get(potentialMappingClass);
			if (objectKey!=null)
			{
				String mappingClassName = potentialMappingClass.getSimpleName();
				String parentClassName = parentClass.getSimpleName();
				String childClassName = dbClass.getSimpleName();
				
				String normalizedParentClassName = parentClassName;
				if (parentClassName.endsWith("Revision"))
				{
					// Remove the "Revision" from the parent table name for the check,
					//  since the naming convention for mapping tables is not strict in this respect.
					normalizedParentClassName = parentClassName.substring(0, parentClassName.indexOf("Revision"));
				}
				
				if (mappingClassName.contains(normalizedParentClassName) && mappingClassName.contains(childClassName))
				{
					// Gotcha!
					return potentialMappingClass;
				}
			}
		}
		return null;
	}
	
	private void checkMappingClass(ClassInfo mappingClassInfo, Class<? extends BusObject> parentClass)
	{
		// First check presence of UID elements.
		checkUIDElements(mappingClassInfo);
		
		// Now check whether the uid elements actually are on the proper foreign keys
		String[] uidElements = mappingClassInfo.getUIDElements();
		if (uidElements.length==0)
		{
			// Check already done in checkUIDElements. No need to search any further for errors.
			return;
		}
		
		// Keep track of all those UID elements that are part of the Primary Key, but are not in either of the foreign keys.
		//  This will help in better reporting.
		boolean[] invalidPrimaryKeyFields = new boolean[uidElements.length];
		for (int j=0; j<uidElements.length; j++)
		{
			// By default, all the PK elements are invalid.
			invalidPrimaryKeyFields[j] = true;
		}
		
		// Now iterate the foreign key relationship with the parent, and check each attribute.
		RelationInfo_FK parentFK = BusObjectHelper.getForeignKeyRelation(mappingClass, parentClass);
		String[] localAttributes = parentFK.getLocalAttributes();
		for (int i=0; i<localAttributes.length; i++)
		{
			boolean foundFKAttributeInPrimaryKey = false;
			for (int j=0; j<uidElements.length; j++)
			{
				if (uidElements[j].equals(localAttributes[i]))
				{
					foundFKAttributeInPrimaryKey = true;
					invalidPrimaryKeyFields[j] = false; // Ok, this one is valid then.
				}
			}
			if (!foundFKAttributeInPrimaryKey) // This one is missing from the PK. Add a message.
			{
				crom.addConfigurationError("The attribute "+localAttributes[i]+" in table "+mappingClassInfo.getTableName()+" is not part of the primary key, but it should be part of it");
			}
		}
		
		// Now iterate the foreign key relationship with the db class itself, and check each attribute.
		RelationInfo_FK childFK = BusObjectHelper.getForeignKeyRelation(mappingClass, dbClass);
		localAttributes = childFK.getLocalAttributes();
		for (int i=0; i<localAttributes.length; i++)
		{
			boolean foundFKAttributeInPrimaryKey = false;
			for (int j=0; j<uidElements.length; j++)
			{
				if (uidElements[j].equals(localAttributes[i]))
				{
					foundFKAttributeInPrimaryKey = true;
					invalidPrimaryKeyFields[j] = false; // Ok, this one is valid then.
				}
			}
			if (!foundFKAttributeInPrimaryKey) // This one is missing from the PK. Add a message.
			{
				crom.addConfigurationError("The attribute "+localAttributes[i]+" in table "+mappingClassInfo.getTableName()+" is not part of the primary key, but it should be part of it");
			}
		}
		
		// Now iterate all the PK attributes. If one of them is not part of the FK relationship, crom cannot deal 
		//  with it, hence it is improper and has to be removed from the PK.
		for (int j=0; j<uidElements.length; j++)
		{
			if (invalidPrimaryKeyFields[j])
			{
				crom.addConfigurationError("The attribute "+uidElements[j]+" in table "+mappingClassInfo.getTableName()+" is part of the primary key, but it should be not part of it. Only the foreign keys attributes should be in the primary key of a mapping table");
			}
		}
		
	}
	
	private void determineMappingRelation()
	{
		mappingClass = getMappingRelation(parentMetadata.dbClass);
		if (mappingClass!=null)
		{
			ClassInfo mappingClassInfo = BusObject._getClassInfo(mappingClass);
			checkMappingClass(mappingClassInfo, parentMetadata.dbClass);
			return;
		}
		
		
		// If no mapping class is found, then there might be a relationship with the Revision class of the parent.
		//  Let's check that, if it is available
		if (parentMetadata.dbRevisionClass!=null)
		{
			mappingClass = getMappingRelation(parentMetadata.dbRevisionClass);
			if (mappingClass!=null)
			{
				ClassInfo mappingClassInfo = BusObject._getClassInfo(mappingClass);
				checkMappingClass(mappingClassInfo, parentMetadata.dbRevisionClass);
				return;
			}
			crom.addConfigurationError("Could not find a database relationship between "+parentMetadata.domainClass.getSimpleName()+".\nCannot find a database relationship between "+dbClass.getSimpleName()+" and "+parentMetadata.dbRevisionClass.getSimpleName());
		}
		else
		{
			crom.addConfigurationError("Error while creating BusObjectMetadata for "+parentMetadata.domainClass.getSimpleName()+".\nCannot find a database relationship between "+dbClass.getSimpleName()+" and "+parentMetadata.dbClass.getSimpleName());
		}
	}
	
	private void checkParentInfo()
	{
		if (parentMetadata==null)
		{
			return;
		}
		
		String relationFromParentName = relationFromParent.getName();
		
		
		HashMap<Class<? extends BusObject>, List<RelationInfo_FK>> myForeignKeys = BusObjectHelper.getForeignKeyRelations(dbClass);
		
		List<RelationInfo_FK> parentDBClassRelations = myForeignKeys.get(parentMetadata.dbClass);
		List<RelationInfo_FK> parentDBRevisionClassRelations = myForeignKeys.get(parentMetadata.dbRevisionClass);
		
		fkParent = getNamedRelation(parentDBClassRelations, relationFromParentName);
		if (fkParent==null)
		{
			fkParent = getNamedRelation(parentDBRevisionClassRelations, relationFromParentName);
		}
		if (fkParent==null)
		{
			fkParent = getAnyRelation(parentDBClassRelations, relationFromParentName);
		}
		if (fkParent==null)
		{
			fkParent = getAnyRelation(parentDBRevisionClassRelations, relationFromParentName);
		}
		if (fkParent==null)
		{
			// TODO: this is yet to be implemented.
			determineMappingRelation();
		}
	}
	
	void registerListeners()
	{
		// Register listeners with the WS-AppServer framework
		getDomainClassInfo().addBeforeCommitListeners(this);
		getDomainClassInfo().addBeforeAbortListeners(this);
		getDomainClassInfo().addBeforeInsertListener(this);
		getDomainClassInfo().addBeforeDeleteListener(this);
		getDomainClassInfo().addBeforeUpdateListener(this);
		getDomainClassInfo().addAfterAbortListener(this);
		getDomainClassInfo().addAfterCommitListener(this);
		getDomainClassInfo().addAfterInsertListener(this);
		getDomainClassInfo().addAfterDeleteListener(this);
		getDomainClassInfo().addAfterUpdateListener(this);
	}
	
	void unregisterListeners()
	{
		// Register listeners with the WS-AppServer framework
		getDomainClassInfo().removeBeforeCommitListeners(this);
		getDomainClassInfo().removeBeforeAbortListeners(this);
		getDomainClassInfo().removeBeforeInsertListener(this);
		getDomainClassInfo().removeBeforeDeleteListener(this);
		getDomainClassInfo().removeBeforeUpdateListener(this);
		getDomainClassInfo().removeAfterAbortListener(this);
		getDomainClassInfo().removeAfterCommitListener(this);
		getDomainClassInfo().removeAfterInsertListener(this);
		getDomainClassInfo().removeAfterDeleteListener(this);
		getDomainClassInfo().removeAfterUpdateListener(this);
	}
	
	/**
	 * Returns the annotation from the class or one of it's superclasses.
	 * Returns null if the annotation was not found.
	 * @param <T> An annotation
	 * @param annotationClass The annotation class to look for
	 * @param clazz The clazz to look on
	 * @return The annotation if found, otherwise null.
	 */
	<T extends Annotation> T getAnnotation(Class<T> annotationClass)
	{
		Class<?> clazz = domainClass;
		while (!clazz.equals(Object.class))
		{
			if (clazz.isAnnotationPresent(annotationClass)) return clazz.getAnnotation(annotationClass);
			clazz = clazz.getSuperclass();
		}
		return null;
	}
	
	/**
	 * Returns the ClassInfo object for the database mapping class of the domain object
	 * @return
	 */
	public ClassInfo getDbClassInfo()
	{
		return dbClassInfo;
	}

	/**
	 * Returns the class that belongs to the database representation of the domain object. Typically used for quick instantiation.
	 * E.g., for crom.example.domain.Ticket this will return crom.example.db.Ticket
	 */
	public Class<? extends BusObject> getDbClass()
	{
		return dbClass;
	}

	/**
	 * Returns ClassInfo of the domain object. E.g., for Ticket this contains the metadata of the crom.example.domain.Ticket class 
	 */
	public ClassInfo getDomainClassInfo()
	{
		return domainClassInfo;
	}
	
	private void checkDomainClassInfo()
	{
		if (domainClassInfo==null)
		{
			crom.addConfigurationError("Fatal error: ClassInfo for domain class "+domainClass+" cannot be found");
			return;
		}
		
		String[] uidElements = this.domainClassInfo.getUIDElements();
		if (uidElements.length==0)
		{
			crom.addConfigurationError(domainClass+" has no UID elements, but they are necessary. This can be done in WS-AppServer by setting the 'unique' attribute to one of the fields of the class");
			return;
		}
	}
	
	/**
	 * Returns the persistence class that relates this metadata to it's parent. This is typically the case for
	 * n-m relationships. E.g., in Ticket example this relationship exists between Project and Person via the Project_Person table.
	 * @param parentClass - The db class of the parent object. Can be either a "plain" table or a "revision" table.
	 * @return
	 */
	public Class<? extends BusObject> getMappingClass()
	{
		return mappingClass;
	}
	
	/**
	 * Returns the persistence class that relates this metadata to it's parent. This is typically the case for
	 * n-m relationships. E.g., in Ticket example this relationship exists between Project and Person via the Project_Person table.
	 * @param parentClass - The db class of the parent object. Can be either a "plain" table or a "revision" table.
	 * @return
	 */
	@Deprecated
	public Class<? extends BusObject> getMappingClass(Class<? extends BusObject> parentClass)
	{
		return getMappingClass();
	}
	
	/**
	 * Returns the class for the revision table of the domain object.
	 * This information is continuously improved by the users of the system. Each improvement is stored as a separate
	 * record in the table (according to the so-called revisioning pattern).
	 * E.g., for Solution, this is "crom.example.db.SolutionRevision.class".
	 */
	public Class<? extends BusObject> getDbRevisionClass()
	{
		return dbRevisionClass;
	}
	
	/**
	 * Determines the information of the DB class corresponding to the DomainObject.
	 * Reads both dbClass and dbClassInfo.
	 */
	@SuppressWarnings("unchecked")
	private Class<? extends BusObject> findDbClass()
	{
		DomainObject domainObjectAnnotation = getAnnotation(DomainObject.class);
		Class<? extends BusObject> dbClass = domainObjectAnnotation.table();
		if (dbClass.equals(BusObject.class))
		{
			String dbClassName = crom.getDBPackage()+domainClass.getSimpleName();
			try
			{
				Class<?> clazz = crom.loadClass(dbClassName);
				if (BusObject.class.isAssignableFrom(clazz))
				{
					dbClass = (Class<? extends BusObject>) clazz;
				}
				else
				{
					crom.addConfigurationError("The class <"+dbClassName+"> must extend BusObject");
				}
			}
			catch (ClassNotFoundException e)
			{
				crom.addConfigurationError("Domain class "+domainClass+" does not have a corresponding database class <"+dbClassName+"> (ClassNotFoundException)");
			}
		}
		return dbClass;
	}
	
	/**
	 * Checks whether the UID elements of the database table are present. If not, then an error is given
	 * that the primary key information is missing.
	 * @param classInfo
	 */
	private void checkUIDElements(ClassInfo classInfo)
	{
		String[] uidElements = classInfo.getUIDElements();
		if (uidElements==null)
		{
			crom.addConfigurationError("Table "+classInfo.getTableName()+" has no UID elements (e.g. due to missing primary key)");
		}
		if (uidElements.length==0)
		{
			crom.addConfigurationError("Table "+classInfo.getTableName()+" has no UID elements (e.g. due to missing primary key)");
		}
	}

	/**
	 * Determines the information of the revision class corresponding to the DomainObject.
	 * Reads both dbClass and dbClassInfo.
	 */
	@SuppressWarnings("unchecked") // Casting check is done in code
	private Class<? extends BusObject> findDbRevisionClass()
	{
		RevisionedObject revisionAnnotation = getAnnotation(RevisionedObject.class);
		Class<? extends BusObject> dbRevisionClass = revisionAnnotation.table_revision();
		if (dbRevisionClass.equals(BusObject.class))
		{
			String dbRevisionClassName = crom.getDBPackage()+domainClass.getSimpleName()+"Revision";
			try
			{
				Class<?> clazz = crom.loadClass(dbRevisionClassName);
				if (BusObject.class.isAssignableFrom(clazz))
				{
					dbRevisionClass = (Class<? extends BusObject>) clazz;
				}
				else
				{
					crom.addConfigurationError("The class <"+dbRevisionClassName+"> must extend BusObject");
				}
			}
			catch (ClassNotFoundException e)
			{
				crom.addConfigurationError("Domain class "+domainClass+" does not have a corresponding database class <"+dbRevisionClassName+"> (ClassNotFoundException)");
			}
		}
		return dbRevisionClass;
	}
	
	private void checkAttribute(ClassInfo classInfo, String attributeName, Class<?> attributeType)
	{
		// Check for presence and type of CreatedOn and CreatedBy
		AttributeInfo attribute = dbRevisionClassInfo.getAttributeInfo(attributeName);
		if (attribute==null)
		{
			crom.addConfigurationError("The column "+attributeName+" is missing in table "+classInfo.getTableName());
			return;
		}
		if (!attribute.getAttributeClass().equals(attributeType))
		{
			crom.addConfigurationError("The column "+attributeName+" has the wrong type (expected "+attributeType.getSimpleName()+", found "+attribute.getAttributeClass().getSimpleName()+".");
		}
	}
	
	private void checkRevisioningPattern()
	{
		if (dbRevisionClassInfo==null)
		{
			crom.addConfigurationError("Fatal error: ClassInfo for revision class "+dbRevisionClass+" cannot be found");
			return;
		}
		
		// There has to be a primary key defined on the object.
		checkUIDElements(dbRevisionClassInfo);
		
		// Check for presence and type of CreatedOn, ExpiredOn and CreatedBy
		checkAttribute(dbRevisionClassInfo, RevisionedObject.CREATED_BY, String.class);
		checkAttribute(dbRevisionClassInfo, RevisionedObject.CREATED_ON, Date.class);
		checkAttribute(dbRevisionClassInfo, RevisionedObject.EXPIRED_ON, Date.class);

		// This is a check for the implementation of the Revision pattern.
		//  Inside the RevisionObjectEventListener we read the foreign key attribute to create a query
		//  that reads the latest revision number. This assumes that the key has only one field.
		RelationInfo_FK fk = BusObjectHelper.getForeignKeyRelation(this.getDbClass(), this.getDbRevisionClass());
		if (fk==null)
		{
			crom.addConfigurationError("There is no foreign key relationship between "+getDbClass().getSimpleName()+" and "+getDbRevisionClass().getSimpleName());
			return;
		}
		if (!fk.getLocalIsPK())
		{
			crom.addConfigurationError("The foreign key relationship between "+getDbClass().getSimpleName()+" and "+getDbRevisionClass().getSimpleName()+" has the wrong direction");
			return;
		}
		
		// TODO: we believe this code is no longer required; but do not have a test case for it.
		//  Typically there will be only one field, and earlier the RevisionObjectEventListener had a hardcoded assumption to search only
		//   on the first field. But that code has gone, and now ROEL takes navigates all fields of the relationship.
//		if (fk.getRelatedAttributes().length!=1)
//		{
//			// This probably means something is changed in the revisioning pattern. That means that the logic inside this class also has to be modified, since we assume that there is just
//			//  one attribute that links the db object to it's revision object (e.g., for Solution and SolutionRevision it is just SolutionId that establishes the relationship).
//			crom.addConfigurationError("Unexpected number ("+fk.getRelatedAttributes().length+" instead of 1) of columns available to be able to establish the foreign key relationship between dbClass "+dbClass+" and dbRevisionClass "+dbRevisionClass);
//		}
	}
	
	/**
	 * Returns ClassInfo of the revision table with the fact information for the domain object. E.g., for Solution this contains the metadata of
	 * the SolutionRevision table
	 */
	public ClassInfo getDbRevisionClassInfo()
	{
		return dbRevisionClassInfo;
	}

	/**
	 * Returns the class of the Domain Object to which this metadata belongs.
	 * @return
	 */
	public Class<? extends BusObject> getDomainClass()
	{
		return domainClass;
	}

	@Override
	public void onBeforeInsert(BusObjectEvent event)
	{
		if (!event.getObject().getClass().equals(domainClass)) return; // Avoid inheritance issues
		BusObjectWrapper.onBeforeInsert(event);
	}

	@Override
	public void onAfterInsert(BusObjectEvent event)
	{
		if (!event.getObject().getClass().equals(domainClass)) return; // Avoid inheritance issues
		BusObjectWrapper.onAfterInsert(event);
	}

	@Override
	public void onBeforeUpdate(BusObjectUpdateEvent event)
	{
		if (!event.getObject().getClass().equals(domainClass)) return; // Avoid inheritance issues
		BusObjectWrapper.onBeforeUpdate(event);
	}

	@Override
	public void onAfterUpdate(BusObjectUpdateEvent event)
	{
		if (!event.getObject().getClass().equals(domainClass)) return; // Avoid inheritance issues
		BusObjectWrapper.onAfterUpdate(event);
	}
	
	@Override
	public void onBeforeDelete(BusObjectEvent event)
	{
		if (!event.getObject().getClass().equals(domainClass)) return; // Avoid inheritance issues
		BusObjectWrapper.onBeforeDelete(event);
	}

	@Override
	public void onAfterDelete(BusObjectEvent event)
	{
		if (!event.getObject().getClass().equals(domainClass)) return; // Avoid inheritance issues
		BusObjectWrapper.onAfterDelete(event);
	}

	@Override
	public void onBeforeAbort(BeforeAbortObjectEvent event)
	{
		if (!event.getObject().getClass().equals(domainClass)) return; // Avoid inheritance issues
		BusObjectWrapper.onBeforeAbort(event);
	}

	@Override
	public void onBeforeCommit(BeforeCommitObjectEvent event)
	{
		if (!event.getObject().getClass().equals(domainClass)) return; // Avoid inheritance issues
		BusObjectWrapper.onBeforeCommit(event);
	}

	@Override
	public void onAfterAbort(AfterAbortObjectEvent event)
	{
		if (!event.getObject().getClass().equals(domainClass)) return; // Avoid inheritance issues
		BusObjectWrapper.onAfterAbort(event);
	}

	@Override
	public void onAfterCommit(AfterCommitObjectEvent event)
	{
		if (!event.getObject().getClass().equals(domainClass)) return; // Avoid inheritance issues
		BusObjectWrapper.onAfterCommit(event);
	}
}
